import cache from 'utils/cache';
import {handleMapArea} from 'utils/map';

/* global Structure StructureExtension StructureSpawn StructureTower
STRUCTURE_RAMPART TOWER_OPTIMAL_RANGE TOWER_FALLOFF_RANGE TOWER_FALLOFF
OBSTACLE_OBJECT_TYPES BODYPART_COST */
declare global {
	interface Structure {
		heapMemory: StructureHeapMemory;
		isWalkable: () => boolean;
		isOperational: () => boolean;
	}

	interface StructureExtension {
		isBayExtension: () => boolean;
	}

	interface StructureSpawn {
		isBaySpawn: () => boolean;
		calculateCreepBodyCost;
		getSpawnDirections: () => DirectionConstant[];
	}

	interface StructureTower {
		getPowerAtRange;
	}

	interface StructureFactory {
		getEffectiveLevel: () => number;
	}

	interface StructureHeapMemory {}
}

// @todo Periodically clear heap memory of deceased creeps.
const structureHeapMemory: Record<string, StructureHeapMemory> = {};

// Define quick access property creep.heapMemory.
Object.defineProperty(Structure.prototype, 'heapMemory', {

	/**
	 * Gets semi-persistent memory for a structure.
	 *
	 * @return {object}
	 *   The memory object.
	 */
	get() {
		if (!structureHeapMemory[this.id]) structureHeapMemory[this.id] = {};

		return structureHeapMemory[this.id];
	},
	enumerable: false,
	configurable: true,
});

/**
 * Checks whether a structure can be moved onto.
 *
 * @return {boolean}
 *   True if a creep can move onto this structure.
 */
Structure.prototype.isWalkable = function () {
	if (_.includes(OBSTACLE_OBJECT_TYPES, this.structureType)) return false;
	if (this.structureType === STRUCTURE_RAMPART) {
		return this.my || this.isPublic;
	}

	return true;
};

/**
 * Replacement for Structure.prototype.isActive that is less CPU intensive.
 *
 * @return {boolean}
 *   True if the structure is operational.
 */
Structure.prototype.isOperational = function (this: Structure) {
	const inactiveStructures = getInactiveStructures(this.room);

	if (inactiveStructures[this.id]) return false;

	return true;
};

function getInactiveStructures(room: Room): Partial<Record<Id<Structure>, boolean>> {
	const rcl = room.controller?.level ?? 0;
	if (rcl >= 8) return {};

	return cache.inHeap('inactiveStructures:' + room.name + ':' + rcl, 500, () => {
		const inactiveStructures = {};
		_.each(room.myStructuresByType, (structures, structureType) => {
			// Check if more structures than allowed exist.
			if (!CONTROLLER_STRUCTURES[structureType] || structures.length <= CONTROLLER_STRUCTURES[structureType][rcl]) return;

			for (const structure of structures) {
				if (!structure.isActive()) {
					inactiveStructures[structure.id] = true;
				}
			}
		});

		return inactiveStructures;
	});
}

/**
 * Checks whether this extension belongs to any bay.
 *
 * @return {boolean}
 *   True if the extension is part of a bay.
 */
StructureExtension.prototype.isBayExtension = function () {
	if (!this.bayChecked) {
		this.bayChecked = true;
		this.bay = null;

		for (const bay of this.room.bays) {
			if (bay.hasExtension(this)) {
				this.bay = bay;
				break;
			}
		}
	}

	return this.bay !== null;
};

StructureSpawn.prototype.isBaySpawn = StructureExtension.prototype.isBayExtension;

StructureSpawn.prototype.getSpawnDirections = function (this: StructureSpawn): DirectionConstant[] {
	if (!this.room.roomPlanner) return undefined;

	return cache.inHeap('spawnDir:' + this.name, 2500, () => {
		const directions = [];
		const terrain = this.room.getTerrain();

		handleMapArea(this.pos.x, this.pos.y, (x, y) => {
			if (x === this.pos.x && y === this.pos.y) return;
			if (terrain.get(x, y) === TERRAIN_MASK_WALL) return;

			const position = new RoomPosition(x, y, this.pos.roomName);
			if (!this.room.roomPlanner.isPlannedLocation(position, STRUCTURE_ROAD)) return;
			if (this.room.roomPlanner.isPlannedLocation(position, 'bay_center')) return;
			if (_.some(position.lookFor(LOOK_STRUCTURES), s => (OBSTACLE_OBJECT_TYPES as string[]).includes(s.structureType))) return;

			directions.push(this.pos.getDirectionTo(position));
		});

		if (directions.length === 0) return undefined;

		return directions;
	});
};

/**
 * Calculates relative tower power at a certain range.
 *
 * @param {number} range
 *   Tile distance between tower and target.
 *
 * @return {number}
 *   Relative power between 0 and 1.
 */
StructureTower.prototype.getPowerAtRange = function (this: StructureTower, range: number) {
	if (range < TOWER_OPTIMAL_RANGE) range = TOWER_OPTIMAL_RANGE;
	if (range > TOWER_FALLOFF_RANGE) range = TOWER_FALLOFF_RANGE;

	return 1 - (((range - TOWER_OPTIMAL_RANGE) / (TOWER_FALLOFF_RANGE - TOWER_OPTIMAL_RANGE)) * TOWER_FALLOFF);
};

/**
 * Calculates the cost of a creep's body parts.
 *
 * @param {object} bodyMemory
 *   An object keyed by body part type, with number of parts as values.
 *
 * @return {number}
 *   The total cost in energy units.
 */
StructureSpawn.prototype.calculateCreepBodyCost = function (bodyMemory) {
	// @todo This really doesn't need to be a method of StructureSpawn.
	let cost = 0;
	_.each(bodyMemory, (count, partType) => {
		cost += BODYPART_COST[partType] * count;
	});

	return cost;
};

/**
 * Calculates the level this factory should be considered to have.
 *
 * Even if the level is still 0, if a power creep with PWR_OPERATE_FACTORY
 * exists in the room, that power's level is returned.
 */
StructureFactory.prototype.getEffectiveLevel = function (this: StructureFactory): number {
	return cache.inObject(this.heapMemory, 'effectiveLevel', 500, () => {
		for (const name in this.room.powerCreeps) {
			const powerCreep = this.room.powerCreeps[name];

			if (powerCreep.powers[PWR_OPERATE_FACTORY]) return powerCreep.powers[PWR_OPERATE_FACTORY].level;
		}

		return 0;
	});
};

export {};
